package dslab.mailbox;

import dslab.core.DMAPProtocol;
import dslab.util.Config;
import dslab.util.Message;
import dslab.util.RemoveMessageTask;
import dslab.util.WriteTask;

import java.io.*;
import java.net.Socket;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;

/**
 * Handler class that is runnable and should run in its own thread;
 * implements the DMAPProtocol, which forces it to implement all methods that are required to deal with DMAP
 */
public class MailboxDMAPHandler implements Runnable, DMAPProtocol {

    // 30 seconds timeout constant
    private static final int TIMEOUT = 30*1000;

    private final String componentId;
    private final Socket socket;
    private final Config users;
    private final BlockingQueue<WriteTask> writeTasks;
    private final Map<String, List<Message>> mails;

    private SessionState state;
    private String user;

    /**
     * enum representing possible states of this particular DMAP protocol implementation
     */
    protected enum SessionState {
        NotLoggedIn, LoggedIn, Done
    }

    /**
     * creates a new MailboxDMAPHandler client connection handler
     * @param componentId string representation of component
     * @param socket client connection socket that was already accepted elsewhere
     * @param users configuration of users
     * @param writeTasks queue of write tasks that will be delegated to WriteQueueHandler
     * @param mails reference to map with mapping of users to list of messages
     * @throws IOException exception if the socket cannot be configured
     */
    public MailboxDMAPHandler(String componentId, Socket socket, Config users,
                              BlockingQueue<WriteTask> writeTasks,
                              Map<String, List<Message>> mails) throws IOException {
        this.componentId = componentId;
        this.socket = socket;
        this.users = users;
        this.writeTasks = writeTasks;
        this.mails = mails;
        this.state = SessionState.NotLoggedIn;
        // register timeout
        this.socket.setSoTimeout(TIMEOUT);
    }

    @Override
    public void run() {
        // TODO: consider generalizing this to the protocol interface as a default implementation
        // TODO: this is hard due to the SessionState enum being different for different protocols
        System.out.printf("@%s: begin run%n", componentId);
        // read and service request on socket
        try (
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
                PrintWriter out = new PrintWriter(socket.getOutputStream(), true)
        ) {
            // announce spoken protocols
            initMessage(out);

            // loop over requests (individual lines)
            String input;
            while ((input = in.readLine()) != null) {
                System.out.printf("@%s: processing input %s%n", componentId, input);
                // handle line
                processRequest(out, input);

                // break on quit after handling quit
                if (state == SessionState.Done)
                    break;
            }

            // finally close connection
            if (!socket.isClosed())
                socket.close();
        } catch (IOException | InterruptedException exception) {
            System.err.printf("@%s: a fatal error has occurred: the connection was interrupted%n", componentId);
            exception.printStackTrace();
        }
        System.out.printf("@%s: end run%n", componentId);
    }

    @Override
    public void initMessage(Writer out) throws IOException {
        // communicate result
        out.write(String.format("ok DMAP%n"));
        out.flush();
    }

    @Override
    public void handleLogin(Writer out, String username, String password) throws IOException {
        if (state == SessionState.LoggedIn) {
            raiseWarning(out, String.format("error already logged in%n"));
            return;
        }

        // exit pre-emptively if user doesn't exist
        if (!users.containsKey(username)) {
            raiseWarning(out, String.format("error unknown user%n"));
            return;
        }

        // exit pre-emptively if password is wrong
        if (!users.getString(username).equals(password)) {
            raiseWarning(out, String.format("error wrong password%n"));
            return;
        }

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        user = username;
        state = SessionState.LoggedIn;
    }

    @Override
    public void handleList(Writer out) throws IOException {
        if (isUnauthenticated(out))
            return;

        // query messages
        List<Message> messages = mails.get(user);
        if (messages.size() == 0) {
            raiseWarning(out, String.format("no messages%n"));
            return;
        }

        // communicate result
        int i = 1;
        for (Message m : messages)
            out.write(String.format("%d %s %s%n", i++, m.sender, m.subject));
        out.flush();
    }

    @Override
    public void handleShow(Writer out, int id) throws IOException {
        if (isUnauthenticated(out))
            return;

        // query messages
        List<Message> messages = mails.get(user);
        if (id > messages.size()) {
            raiseWarning(out, String.format("error id does not exist%n"));
            return;
        }

        // get message
        Message message = messages.get(id - 1);
        // communicate result
        out.write(String.format(
            "from %s%nto %s%nsubject %s%ndata %s%n",
            message.sender,
            message.recipients,
            message.subject,
            message.data
        ));
        out.flush();
    }

    @Override
    public void handleDelete(Writer out, int id) throws IOException {
        if (isUnauthenticated(out))
            return;

        // query messages
        List<Message> messages = mails.get(user);
        if (id > messages.size()) {
            raiseWarning(out, String.format("error id does not exist%n"));
            return;
        }

        // put message to blocking queue that handles write tasks (in a separate thread)
        // this model adheres to the single writer principle
        // TODO: this was added in a recent refactor and not adequately tested
        //       id mechanism might need to be revised, as user can insert a different
        //       message in another session and change the meanings of id numbers
        //       this is probably relatively unimportant however, as an unlikely
        //       race condition
        writeTasks.add(
            new RemoveMessageTask(
                String.format("%s-remove-message-task", componentId),
                id,
                user,
                mails
            )
        );

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
    }

    @Override
    public void handleLogout(Writer out) throws IOException {
        if (isUnauthenticated(out))
            return;

        // communicate result
        out.write(String.format("ok%n"));
        out.flush();
        state = SessionState.NotLoggedIn;
    }

    @Override
    public void handleQuit(Writer out) throws IOException {
        // communicate result
        out.write(String.format("ok bye%n"));
        out.flush();
        state = SessionState.Done;
    }

    @Override
    public void raiseWarning(Writer out, String warning) throws IOException {
        // communicate error
        out.write(warning);
        out.flush();
    }

    @Override
    public void raiseError(Writer out, String error) throws IOException {
        raiseWarning(out, error);
        state = SessionState.Done;
    }

    /**
     * verifies whether the session has already processed a user login successfully
     * @param out writer bound to client connection output stream
     * @return true if session is unauthenticated
     * @throws IOException in case of a broken connection or writer an exception is thrown
     */
    private boolean isUnauthenticated(Writer out) throws IOException {
        boolean authenticated = state == SessionState.LoggedIn;
        if (!authenticated) raiseWarning(out, String.format("error not logged in%n"));
        return !authenticated;
    }
}
